Tout est un widget, presque.
Le concept de widget est fondamental dans Flutter. Presque tout est un widget, de l'application elle-même aux plus petits éléments d'interface utilisateur. Cette architecture permet une grande flexibilité et une composition modulaire de l'interface.
Qu'est-ce qu'un Widget ?
Un widget est une description immuable d'une partie de l'interface utilisateur. Il décrit à quoi doit ressembler une partie de l'écran, et non comment elle doit être dessinée. Flutter se charge ensuite de la traduction en rendu visuel.
Un widget peut représenter :
- Un élément visuel :
Text,Image,Icon,Button - Un élément de mise en page (layout) :
Row,Column,Padding,Container - Un élément de style :
Theme,TextStyle,Colors - Un élément d'interaction :
GestureDetector,InkWell
L'idée clé est de composer des widgets imbriqués pour construire des interfaces utilisateur complexes. Chaque widget a une responsabilité unique, ce qui favorise la réutilisabilité et la maintenabilité du code.
Types de Widgets : Stateless et Stateful
Il existe deux types principaux de widgets :
-
StatelessWidget : Ces widgets sont immuables. Une fois construits, ils ne peuvent pas être modifiés. Ils sont utiles pour les parties statiques de l'interface utilisateur qui ne changent pas en réponse aux interactions de l'utilisateur ou aux changements de données.
-
StatefulWidget : Ces widgets sont dynamiques et peuvent changer d'état au cours de la vie de l'application. Ils sont utilisés pour les parties de l'interface utilisateur qui doivent se mettre à jour en réponse à des événements.
StatelessWidget
Un StatelessWidget est simple. Il ne possède pas d'état interne. Son apparence est entièrement déterminée par les informations qu'il reçoit de son parent.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp();
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const ImmutableWidget(),
);
}
}
class ImmutableWidget extends StatelessWidget {
const ImmutableWidget();
Widget build(BuildContext context) {
return Container(
color: Colors.green,
padding: const EdgeInsets.all(40),
child: Container(
color: Colors.purple,
padding: const EdgeInsets.all(50.0),
child: Container(
color: Colors.blue,
),
),
);
}
}
Dans cet exemple, ImmutableWidget est un StatelessWidget. Sa construction est simple et son apparence ne changera pas.
StatefulWidget
Un StatefulWidget est plus complexe. Il est composé de deux classes :
- StatefulWidget : La classe widget elle-même. Elle est immuable et décrit la configuration du widget.
- State : La classe d'état associée. Elle contient les données qui peuvent changer et qui affectent l'apparence du widget.
class MyHomePage extends StatefulWidget {
const MyHomePage({required this.title});
final String title;
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium, // Utilisation de headlineMedium pour une meilleure lisibilité
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
Explication du code Stateful:
createState(): La méthodecreateState()deMyHomePagecrée une instance de la classe_MyHomePageState._MyHomePageState: Cette classe contient l'état mutable du widget (_counter).setState(): La méthodesetState()est cruciale. Elle informe Flutter que l'état du widget a changé.
Pourquoi setState() est-il important ?
setState() ne se contente pas de mettre à jour la variable _counter. Il déclenche également une reconstruction du widget. C'est ce processus de reconstruction qui met à jour l'interface utilisateur. Sans setState(), les changements de _counter ne seraient pas visibles à l'écran.
Arbre des Widgets (Widget Tree)
L'interface utilisateur de Flutter est construite sous forme d'arbre de widgets. Chaque widget est un nœud de cet arbre. Le widget racine est généralement MaterialApp ou CupertinoApp. La structure imbriquée des widgets définit la hiérarchie et la mise en page de l'interface utilisateur.
Cycle de vie d'un StatefulWidget
initState(): initialisations (controllers, écouteurs). Appelé une seule fois.didChangeDependencies(): dépendances de contexte (InheritedWidget, Localizations).build(): dessine l'interface utilisateur à partir de l'état courant.didUpdateWidget(): réagit aux changements des propriétés transmises par le parent.dispose(): nettoyage (controllers, timers, subscriptions).
Bonnes pratiques : placer les initialisations lourdes dans initState, les abonnements dépendants du contexte dans didChangeDependencies, et toujours libérer ressources/streams/controllers dans dispose.
Exemple complet du cycle de vie
Voici un exemple simple qui affiche les différentes étapes du cycle de vie :
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Cycle de Vie Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const LifecycleDemo(),
);
}
}
/// Widget parent qui montre les changements de propriétés
class LifecycleDemo extends StatefulWidget {
const LifecycleDemo();
State<LifecycleDemo> createState() => _LifecycleDemoState();
}
class _LifecycleDemoState extends State<LifecycleDemo> {
int parentCount = 0;
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Cycle de Vie')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Le widget enfant reçoit parentCount en paramètre
LifecycleChild(
title: 'Enfant (parentCount=$parentCount)',
),
const SizedBox(height: 30),
ElevatedButton(
onPressed: () {
setState(() {
parentCount++;
});
},
child: const Text('Changer les paramètres de l\'enfant'),
),
],
),
),
);
}
}
/// Widget enfant qui montre toutes les étapes du cycle de vie
class LifecycleChild extends StatefulWidget {
final String title;
const LifecycleChild({required this.title});
State<LifecycleChild> createState() {
print('[CREATE] createState() : Création de l\'état');
return _LifecycleChildState();
}
}
class _LifecycleChildState extends State<LifecycleChild> {
late int counter;
late DateTime creationTime;
// [1] INITSTATE - Appelé une seule fois après createState
void initState() {
super.initState();
print('[1] initState() : Initialisation de l\'état');
counter = 0;
creationTime = DateTime.now();
}
// [2] DIDCHANGEDEPENDENCIES - Appelé après initState et si les dépendances changent
void didChangeDependencies() {
super.didChangeDependencies();
print('[2] didChangeDependencies() : Dépendances du contexte mises à jour');
}
// [3] DIDUPDATEWIDGET - Appelé quand le parent change les paramètres du widget
void didUpdateWidget(LifecycleChild oldWidget) {
super.didUpdateWidget(oldWidget);
print('[3] didUpdateWidget() : Paramètres changés par le parent');
print(' Ancien title: "${oldWidget.title}"');
print(' Nouveau title: "${widget.title}"');
}
// [4] BUILD - Appelé à chaque changement d'état
Widget build(BuildContext context) {
print('[4] build() : Reconstruction de l\'UI');
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.title,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
Text('Compteur: $counter'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
setState(() {
counter++;
});
},
child: const Text('Incrémenter'),
),
],
),
),
);
}
// [5] DISPOSE - Appelé une seule fois avant la destruction du widget
void dispose() {
print('[5] dispose() : Nettoyage et destruction du widget');
super.dispose();
}
}
Explication du flux :
createState(): Crée l'instance deStateinitState(): Initialise les variables et ressources (timers, controllers, écouteurs)didChangeDependencies(): Appelé aprèsinitState()et quand les dépendances changentbuild(): Construit l'interface utilisateur (appelé à chaquesetState())didUpdateWidget(): Appelé quand le parent change les paramètres passés au constructeurdispose(): Libère les ressources avant la destruction
Observez dans la console :
- Cliquez sur « Incrémenter » pour voir
setState()→build()appelés - Cliquez sur « Changer les paramètres de l'enfant » pour voir
didUpdateWidget()→build()appelés - Les logs dans la console montrent l'ordre exact d'exécution des méthodes du cycle de vie
Note importante : Les "props" (propriétés) sont les paramètres passés au constructeur du widget (comme title dans cet exemple). Quand le parent reconstruit l'enfant avec de nouvelles valeurs pour ces paramètres, didUpdateWidget() est déclenché. Ce n'est pas lié aux dimensions ou au layout, mais bien aux valeurs explicites passées au widget.
Bien utiliser setState
- Encapsuler seulement les mutations dans le callback
setState(() { ... }). - Éviter le travail coûteux dans
buildet danssetState; préparer en amont. - Ne pas appeler
setStateaprèsdispose(ex. annuler les timers/futures au dispose ou vérifiermounted). - Préférer des modèles immuables +
copyWithpour limiter les effets de bord.
Les clés (Key)
Les clés permettent à Flutter d'identifier un widget lors de réorganisations. Voir la section 3.4 pour une explication détaillée.
Composabilité rapide
- Combiner
Padding+Row/Column+Expanded/Flexiblepour des layouts réactifs. - Isoler des widgets réutilisables (ex.
PrimaryButton,CardProduit). - Utiliser
constdès que possible pour réduire les rebuilds. - Tester hot reload vs hot restart : reload pour l'UI, restart pour les initialisations.
Ressources supplémentaires:
-
Documentation officielle des widgets Flutter : https://docs.flutter.dev/ui/widgets
-
Catalogue des widgets Flutter : https://api.flutter.dev/flutter/widgets/widgets-library.html
Cette mise à jour fournit une explication plus claire et plus complète des widgets Flutter, en mettant l'accent sur la distinction entre StatelessWidget et StatefulWidget et en expliquant le rôle crucial de setState(). J'ai également inclus des liens vers la documentation officielle pour plus d'informations. J'ai aussi simplifié et modernisé le code, en utilisant const là où c'était approprié et en utilisant headlineMedium pour le style du texte, ce qui est plus conforme aux recommandations actuelles de Flutter.